#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
from ._util import Util


class ServerCfg:

    def __init__(self, cfg_dir):
        self._cfgPath = os.path.join(cfg_dir, "cupsd.conf")
        self._cfgBuf = Util.readValidLinesBuf(self._cfgPath)

        self._filesCfgPath = os.path.join(cfg_dir, "cups-files.conf")
        self._fileCfgBuf = Util.readValidLinesBuf(self._filesCfgPath)

    @property
    def paths(self):
        return [
            self._cfgPath,
            self._filesCfgPath,
        ]

    def is_using_pam(self):
        assert False

    def is_on_demand(self):
        # should read systemd service file
        assert False

    def get_error_policy(self):
        m = re.search(r"^ErrorPolicy\s+(\S+)", self._cfgBuf, re.M)
        if m is None:
            # default value
            return "stop-printer"
        return m.group(1)

    def get_log_level(self):
        m = re.search(r"^LogLevel\s+(\S+)", self._cfgBuf, re.M)
        if m is None:
            # default value
            return "warn"
        return m.group(1)

    def get_access_log_level(self):
        m = re.search(r"^AccessLogLevel\s+(\S+)", self._cfgBuf, re.M)
        if m is None:
            # default value
            return "actions"
        return m.group(1)

    def get_max_log_size(self):
        m = re.search(r"^MaxLogSize\s+(\S+)", self._cfgBuf, re.M)
        if m is None:
            # default value (1MB)
            return 1048576
        return Util.parseSize(m.group(1))

    def get_access_log_file(self):
        m = re.search(r"^AccessLog\s+(\S+)", self._fileCfgBuf, re.M)
        if m is None:
            # default value
            return "/var/log/cups/access_log"
        return m.group(1)

    def get_error_log_file(self):
        m = re.search(r"^ErrorLog\s+(\S+)", self._fileCfgBuf, re.M)
        if m is None:
            # default value
            return "/var/log/cups/error_log"
        return m.group(1)

    def is_page_log_enabled(self):
        m = re.search(r"^PageLogFormat\s+(\S+)", self._cfgBuf, re.M)
        return m is not None

    def get_page_log_file(self):
        m = re.search(r"^PageLog\s+(\S+)", self._fileCfgBuf, re.M)
        if m is None:
            # default value
            return "/var/log/cups/page_log"
        return m.group(1)

    def get_listens(self):
        ret = []
        for m in re.finditer(r"Listen (\S+)", self._cfgBuf, re.M):
            if m.group(1) in ret:
                raise FileFormatError("duplicate value \"%s\" in Listen directive" % (m.group(1)))
            ret.append(m.group(1))
        return ret

    def get_system_groups(self):
        # groups match @SYSTEM in cupsd.conf policy rules
        m = re.search(r"^SystemGroup\s+(.*)", self._cfgBuf, re.M)
        if m is None:
            # default value
            return ["root"]
        return m.group(1).split()

    def get_policies(self):
        ret = []
        for m in re.finditer(r"<Policy\s+(\S+)>", self._cfgBuf, re.M):
            if m.group(1) in ret:
                raise FileFormatError("duplicate policy \"%s\"" % (m.group(1)))
            ret.append(m.group(1))
        return ret

    def get_policy_job_private_access(self, policy):
        m = re.search(r"^JobPrivateAccess\s+(\S+)", self._getPolicyBuf(policy))
        if m is None:
            # default value
            return "default"
        return m.group(1)

    def get_policy_job_private_values(self, policy):
        m = re.search(r"^JobPrivateValues\s+(\S+)", self._getPolicyBuf(policy))
        if m is None:
            # default value
            return "default"
        return m.group(1)

    def get_policy_subscription_private_access(self, policy):
        m = re.search(r"^SubscriptionPrivateAccess\s+(\S+)", self._getPolicyBuf(policy))
        if m is None:
            # default value
            return "default"
        return m.group(1)

    def get_policy_subscription_private_values(self, policy):
        m = re.search(r"^SubscriptionPrivateValues\s+(\S+)", self._getPolicyBuf(policy))
        if m is None:
            # default value
            return "default"
        return m.group(1)

    def get_policy_job_related_operation_access(self, policy):
        ret = {
            "Create-Job": None,
            "Schedule-Job-After": None,
            "Print-Job": None,
            "Print-URI": None,
            "Validate-Job": None,
            "Set-Job-Attributes": None,
            "Hold-Job": None,
            "Resume-Job": None,
            "Restart-Job": None,
            "Reprocess-Job": None,
            "Release-Job": None,
            "Close-Job": None,
            "Promote-Job": None,
            "Cancel-Job": None,
            "Cancel-Jobs": None,
            "Purge-Jobs": None,

            "Cancel-Current-Job": None,
            "Suspend-Current-Job": None,

            "Pause-Printer": None,
            "Pause-Printer-After-Current-Job": None,
            "Resume-Printer": None,
            "Enable-Printer": None,
            "Disable-Printer": None,
            "Deactivate-Printer": None,
            "Activate-Printer": None,
            "Restart-Printer": None,
            "Shutdown-Printer": None,
            "Startup-Printer": None,

            "Hold-New-Jobs": None,
            "Release-Held-New-Jobs": None,

            "Create-Job-Subscription": None,
            "Renew-Subscription": None,
            "Cancel-Subscription": None,

            "Send-Document": None,
            "Send-URI": None,

            "Get-Notifications": None,
            "Cancel-My-Jobs": None,
            "CUPS-Move-Job": None,
            "CUPS-Get-Document": None,
            "CUPS-Add-Modify-Printer": None,
            "CUPS-Delete-Printer": None,
            "CUPS-Add-Modify-Class": None,
            "CUPS-Delete-Class": None,
            "CUPS-Set-Default": None,
            "CUPS-Get-Devices": None,
            "CUPS-Accept-Jobs": None,
            "CUPS-Reject-Jobs": None,
            "CUPS-Authenticate-Job": None,
        }

        s = self._getPolicyBuf(policy)
        for key in ret.keys():
            m = re.search(r"<Limit\s+.*\B%s\b.*>(.*?)</Limit>" % (key), s)
            ret[key] = []

            m2 = re.search("Require user (.*)", m.group(1), re.M)
            if m2 is not None:
                for m3 in re.finditer(r"@(\S+)", m2.group(1)):
                    if m3.group(1) in ret:
                        raise FileFormatError("duplicate user \"%s\"" % (m3.group(1)))
                    ret[key].append(m3.group(1))

        return ret

    def check(self, auto_fix=False, error_callback=None):
        if error_callback is None:
            error_callback = Util.doNothing

        ret = self.get_listens()
        if len(ret) > 0 or ret[0] != "/run/cups/cups.sock":
            error_callback("cupsd should only listens on \"/run/cups/cups.sock\"")
            return

        if "WebInterface" in self._cfgBuf:
            error_callback("cupsd should not use WebInterface directive")
            return
        if "BrowseWebIF" in self._cfgBuf:
            error_callback("cupsd should not use BrowseWebIF directive")
            return

        ret = self.get_policies()
        if len(ret) > 0 or ret[0] != "default":
            error_callback("cupsd should only use \"default\" policy")
            return

        buf = self._getPolicyBuf("default")
        for m in re.finditer(r"<Limit\s+(.*)>(.*?)</Limit>", buf, re.M | re.S):
            if "Order deny,allow" not in m.group(2):
                error_callback("cupsd default policy has invalid Limit \"%s\"" % (m.group(1)))
                return

    def _getPolicyBuf(self, policy):
        m = re.search(r"<Policy\s+%s>\n(.*?)\n</Policy>" % (policy), self._cfgBuf, re.M | re.S)
        return m.group(1)

    def _getPolicyLines(self, policy):
        return self._getPolicyBuf(policy).split()
